/**
 * Copyright (c) 2007-2008 Nearchos Paspallis
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * AspectSense
 *
 * Middleware for annotation-based, context-aware and adaptive services for
 * mobile and ubiquitous computing
 *
 * This project is an open-source effort, initiated by Nearchos Paspallis as
 * part of his PhD work at the University of Cyprus (http://www.ucy.ac.cy).
 *
 * More information about the project is available at "http://aspectsense.org".
 */

package org.aspectsense.framework;

import org.aspectsense.graphs.*;
import org.aspectsense.metadata.ComponentMetadata;
import org.aspectsense.metadata.MetadataFactory;
import org.aspectsense.coreservices.ConfigurationService;

import java.util.*;
import java.util.logging.Logger;

/**
 * @author Nearchos Paspallis [nearchos@cs.ucy.ac.cy]
 *         Date: Dec 20, 2007
 *         Time: 3:45:06 PM
 */
public class ConfigurationManager implements ConfigurationService
{
    private final Logger logger = Logger.getLogger(ConfigurationManager.class.getCanonicalName());

    private final Repository repository = Repository.getInstance();

    // ----------------- Singleton pattern implementation ------------------- //

    private static ConfigurationManager instance = null;

    /**
     * Provides access to the lazy-loaded singleton instance of the
     * {@link org.aspectsense.framework.ConfigurationManager}.
     *
     * @return an instance of {@link ConfigurationManager}
     */
    static ConfigurationManager getInstance()
    {
        synchronized(ConfigurationManager.class)
        {
            if(instance == null) instance = new ConfigurationManager();

            return instance;
        }
    }

    private ConfigurationManager()
    {
        logger.fine("Starting instance of " + ConfigurationManager.class.getCanonicalName());
    }

    // --------------- End of singleton pattern implementation -------------- //

    // -------------- Implementing the decided reconfigurations ------------- //

    /**
     * Maps the resolved services of the current configuration to the
     * corresponding service providers (i.e. components)
     */
    private DefaultDirectedBipartiteGraph<String> currentConfiguration
            = new DefaultDirectedBipartiteGraph<String>();

    /**
     * Applies the provided resolution matrix, thus in effect reconfiguring the
     * system. This is done by initially computing the delta matrices required,
     * and then by applying these changes (e.g. such as stopping or starting
     * components, and destroying or creating links).
     *
     * @param updatedServiceResolutionMatrix the matrix containing the desired
     * service resolutions
     * @throws ConfigurationException if an error occurs while reconfiguring
     * the {@link org.aspectsense.framework.Component}s
     */
    synchronized public void applyServiceResolutionMatrix(final Map<String, String> updatedServiceResolutionMatrix)
            throws ConfigurationException
    {
        // todo We need some improvements to make sure that multiple references to different instances of the same type can be done from within the same component
        if(updatedServiceResolutionMatrix == null)
        {
            throw new NullPointerException("Illegal null argument!");
        }

        // compute the set of current end-user services
//        final Set<Node<String>> currentServiceNodeIDs = currentConfiguration.getLeftPartition();
//        final Set<Node<String>> currentEndUserServiceNodeIDs = selectEndUserNodeServices(currentServiceNodeIDs);

        // compute the set of new (requested) end-user services
        final Set<String> allServiceIDs = updatedServiceResolutionMatrix.keySet();
        final Set<String> endUserServiceIDs = selectEndUserServices(allServiceIDs);
        final Set<Node<String>> endUserServiceNodeIDs = encloseInDefaultNodes(endUserServiceIDs);

        // this is the target configuration to be computed in the following steps
        final DefaultDirectedBipartiteGraph<String> updatedConfiguration
                = new DefaultDirectedBipartiteGraph<String>();

        // first compute the edges directed from left to right (i.e. from services to providers)
        final Set<String> serviceIDs = updatedServiceResolutionMatrix.keySet();
        for(final String serviceID : serviceIDs)
        {
            final String componentID = updatedServiceResolutionMatrix.get(serviceID);

            final Node<String> leftNode = new DefaultNode<String>(serviceID);
            final Node<String> rightNode = new DefaultNode<String>(componentID);
            final DirectedEdge<String> directedEdge = new DefaultDirectedEdge<String>(leftNode, rightNode);

            updatedConfiguration.addEdge(directedEdge, leftNode, rightNode);
        }

        // then compute the edges directed from right to left (i.e. from components to required services)
        final Collection<String> componentIDs = updatedServiceResolutionMatrix.values();
        for(final String componentID : componentIDs)
        {
            final Class componentType = repository.getComponentType(componentID);
            final ComponentMetadata componentMetadata = MetadataFactory.getComponentMetadata(componentType);

            final Collection<RequiredServiceMetadata> requiredServiceMetadataCollection
                    = componentMetadata.getRequiredServices().values();
            for(final RequiredServiceMetadata requiredServiceMetadata : requiredServiceMetadataCollection)
            {
                final String requiredServiceID
                        = requiredServiceMetadata.getServiceTypeCanonicalName();

                // add reverse edge
                final Node<String> fromNode = new DefaultNode<String>(componentID);
                final Node<String> toNode = new DefaultNode<String>(requiredServiceID);
                final DirectedEdge<String> directedEdge = new DefaultDirectedEdge<String>(fromNode, toNode);

                updatedConfiguration.addEdge(directedEdge, toNode, fromNode);
            }
        }

        /* We should never reach this point, because the adaptation manager
         * checks this beforehand
         */
        assert(!currentConfiguration.equals(updatedConfiguration));

        /* Then compute the changes required to transition from the current
         * to the updated configuration.
         */
        final BipartiteGraphDelta<String> delta
                = currentConfiguration.computeDelta(updatedConfiguration);

        // first compute the required deactivation plan
        final DeactivationPlan deactivationPlan;
        if(currentConfiguration.isEmpty())
        {
            deactivationPlan = DeactivationPlan.EMPTY_DEACTIVATION_PLAN;
        }
        else
        {
            final Set<Node<String>> nodesToBeStopped = delta.getRemovedRightNodes();
            final DefaultDirectedBipartiteGraph<String> stopPath = currentConfiguration.computeMinGraphIncludingRightNodes(nodesToBeStopped);
//            deactivationPlan = new DeactivationPlan(stopPath, endUserServiceNodeIDs);
            deactivationPlan = new DeactivationPlan(stopPath);

            logger.finest("stopPath: " + stopPath.toShortString());
        }

        // and then compute the follow-up activation plan
        final ActivationPlan activationPlan;
        if(updatedConfiguration.isEmpty())
        {
            activationPlan = ActivationPlan.EMPTY_ACTIVATION_PLAN;
        }
        else
        {
            final Set<Node<String>> nodesToBeStarted = delta.getAddedRightNodes();
            final DefaultDirectedBipartiteGraph<String> startPath = updatedConfiguration.computeMinGraphIncludingRightNodes(nodesToBeStarted);
//            activationPlan = new ActivationPlan(startPath, endUserServiceNodeIDs);
            activationPlan = new ActivationPlan(startPath);

            logger.finest("startPath: " + startPath.toShortString());
        }

        logger.finest("deactivationPlan: " + deactivationPlan);
        logger.finest("activationPlan: " + activationPlan);

        /* Finally, at this point we proceed to apply the reconfiguration. */

        // 1. Deactivate the required components in the correct order
        try
        {
            final Vector<Node<String>> componentsToBeDeactivated = deactivationPlan.getOrderedNodes();

            for(final Node<String> nodeToBeDeactivated : componentsToBeDeactivated)
            {
                final String componentID = nodeToBeDeactivated.getValue();
                final ComponentContainer componentContainer
                        = currentlyDeployedComponents.get(componentID);

                if(componentContainer == null)
                {
                    throw new ConfigurationException("Could not find deprecated component: " + componentID);
                }
                componentContainer.suspendComponent();
            }
        }
        catch (ConfigurationException ce)
        {
            throw new ConfigurationException("Could not suspend deprecated components!", ce);
        }

        // 2. Destroy the deprecated links in the correct order
        final Vector<DirectedEdge<String>> linksToBeBroken = deactivationPlan.getOrderedDirectedEdges();

        for(final DirectedEdge<String> linkToBeBroken : linksToBeBroken)
        {
            final String sourceComponentID = linkToBeBroken.getFromValue();
            final String targetComponentID = linkToBeBroken.getToValue();

            final ComponentContainer sourceComponentContainer
                    = currentlyDeployedComponents.get(sourceComponentID);
            final ComponentContainer targetComponentContainer
                    = currentlyDeployedComponents.get(targetComponentID);

            if(sourceComponentContainer == null)
            {
                throw new ConfigurationException("Cannot create a link with a null source component!");
            }
            if(targetComponentContainer == null)
            {
                throw new ConfigurationException("Cannot create a link with a null target component!");
            }

            sourceComponentContainer.destroyLink(targetComponentContainer);
        }

        // 3. Return unused components to the repository

        final Set<Node<String>> componentsToBeRemoved = delta.getRemovedRightNodes();
        for(final Node<String> componentToBeRemoved : componentsToBeRemoved)
        {
            final String componentID = componentToBeRemoved.getValue();
            final ComponentContainer componentContainer = currentlyDeployedComponents.get(componentID);
            repository.getComponentContainerPool(componentID).returnComponentContainer(componentContainer);
        }

        // 4. Acquire and instantiate possibly new components

        final Set<Node<String>> componentsToBeAdded = delta.getAddedRightNodes();
        for(final Node<String> componentToBeAdded : componentsToBeAdded)
        {
            final String componentID = componentToBeAdded.getValue();
            final ComponentContainer componentContainer
                    = repository.getComponentContainerPool(componentID).takeComponentContainer();

            currentlyDeployedComponents.put(componentID, componentContainer);
        }

        // 5. Create new links in the right order

        final Vector<DirectedEdge<String>> linksToBeCreated = activationPlan.getOrderedDirectedEdges();

        for(final DirectedEdge<String> linkToBeCreated : linksToBeCreated)
        {
            final String sourceComponentID = linkToBeCreated.getFromValue();
            final String targetComponentID = linkToBeCreated.getToValue();

            final ComponentContainer sourceComponentContainer
                    = currentlyDeployedComponents.get(sourceComponentID);
            final ComponentContainer targetComponentContainer
                    = currentlyDeployedComponents.get(targetComponentID);

            logger.finest("Forming link: " + sourceComponentID + " -> " + targetComponentID);

            if(sourceComponentContainer == null)
            {
                throw new ConfigurationException("Cannot create a link with a null source component!");
            }
            if(targetComponentContainer == null)
            {
                throw new ConfigurationException("Cannot create a link with a null target component!");
            }

            sourceComponentContainer.createLink(targetComponentContainer);
        }

        // 6. Activate the added components in the right order

        try
        {
            final Vector<Node<String>> componentsToBeActivated = activationPlan.getOrderedNodes();

            for(final Node<String> nodeToBeActivated : componentsToBeActivated)
            {
                final String componentID = nodeToBeActivated.getValue();
                final ComponentContainer componentContainer
                        = currentlyDeployedComponents.get(componentID);

                if(componentContainer == null)
                {
                    throw new ConfigurationException("Could not find deprecated component: " + componentID);
                }

                // instantiate component if necessary

                final ComponentLifecycleState componentLifecycleState
                        = componentContainer.getComponentLifecycleState();

                if(componentLifecycleState == ComponentLifecycleState.UNINITIALIZED
                        || componentLifecycleState == ComponentLifecycleState.UNINSTANTIATED)
                {
                    componentContainer.initComponent();
                }

                componentContainer.activateComponent();
            }
        }
        catch (ConfigurationException ce)
        {
            throw new ConfigurationException("Could not activate components!", ce);
        }

        // finally, update the current configuration
        currentConfiguration = updatedConfiguration;
    }

    private Map<String, ComponentContainer> currentlyDeployedComponents
            = new HashMap<String, ComponentContainer>();

    // ---------- End of implementing the decided reconfigurations ---------- //

    private Set<String> selectEndUserServices(Set<String> allServiceIDs)
    {
        final Set<String> endUserServiceIDs = new HashSet<String>();

        // serviceIDs collects all service IDs
        for(final String serviceID : allServiceIDs)
        {
            final Class serviceType = repository.getServiceType(serviceID);
            if(MetadataFactory.isEndUserService(serviceType))
            {
                endUserServiceIDs.add(serviceID);
            }
        }

        return endUserServiceIDs;
    }

    private Set<Node<String>> selectEndUserNodeServices(Set<Node<String>> allServiceNodeIDs)
    {
        final Set<Node<String>> endUserServiceIDs = new HashSet<Node<String>>();

        // serviceIDs collects all service IDs
        for(final Node<String> serviceNodeID : allServiceNodeIDs)
        {
            final String serviceID = serviceNodeID.getValue();
            final Class serviceType = repository.getServiceType(serviceID);
            if(MetadataFactory.isEndUserService(serviceType))
            {
                endUserServiceIDs.add(serviceNodeID);
            }
        }

        return endUserServiceIDs;
    }

    private Set<Node<String>> encloseInDefaultNodes(final Set<String> input)
    {
        final Set<Node<String>> nodeSet = new HashSet<Node<String>>(input.size());

        for(final String s : input)
        {
            nodeSet.add(new DefaultNode<String>(s));
        }

        return nodeSet;
    }
}